/*  PCSX2 - PS2 Emulator for PCs
 *  Copyright (C) 2002-2020  PCSX2 Dev Team
 *
 *  PCSX2 is free software: you can redistribute it and/or modify it under the terms
 *  of the GNU Lesser General Public License as published by the Free Software Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *  PURPOSE.  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with PCSX2.
 *  If not, see <http://www.gnu.org/licenses/>.
 */

//  Adapted from ZeroSPU2 code by Zerofrog. Heavily modified by Arcum42.

#ifdef __linux__

#include <alsa/asoundlib.h>

#include "../Global.h"
#include "Alsa.h"
#include "../SndOut.h"

// Does not work, except as effectively a null plugin.
class AlsaMod : public SndOutModule
{
protected:
	static const int PacketsPerBuffer = 1; // increase this if ALSA can't keep up with 512-sample packets
	static const int MAX_BUFFER_COUNT = 4;
	static const int NumBuffers = 4; // TODO: this should be configurable someday -- lower values reduce latency.
	unsigned int pspeed;

	snd_pcm_t* handle;
	snd_pcm_uframes_t buffer_size;
	snd_async_handler_t* pcm_callback;

	uint period_time;
	uint buffer_time;

protected:
	// Invoked by the static ExternalCallback method below.
	void _InternalCallback()
	{
		snd_pcm_sframes_t avail;
		fprintf(stderr, "* SPU2:Iz in your internal callback.\n");

		avail = snd_pcm_avail_update(handle);
		while (avail >= (int)period_time)
		{
			StereoOut16 buff[PacketsPerBuffer * SndOutPacketSize];
			StereoOut16* p1 = buff;

			for (int p = 0; p < PacketsPerBuffer; p++, p1 += SndOutPacketSize)
				SndBuffer::ReadSamples(p1);

			snd_pcm_writei(handle, buff, period_time);
			avail = snd_pcm_avail_update(handle);
		}
	}

	// Preps and invokes the _InternalCallback above.  This provides a cdecl-compliant
	// entry point for our C++ified object state. :)
	static void ExternalCallback(snd_async_handler_t* pcm_call)
	{
		fprintf(stderr, "* SPU2:Iz in your external callback.\n");
		AlsaMod* data = (AlsaMod*)snd_async_handler_get_callback_private(pcm_call);

		pxAssume(data != nullptr);
		//pxAssume( data->handle == snd_async_handler_get_pcm(pcm_call) );

		// Not sure if we just need an assert, or something like this:
		if (data->handle != snd_async_handler_get_pcm(pcm_call))
		{
			fprintf(stderr, "* SPU2: Failed to handle sound.\n");
			return;
		}

		data->_InternalCallback();
	}

public:
	s32 Init()
	{
		//fprintf(stderr,"* SPU2: Initing Alsa\n");
		snd_pcm_hw_params_t* hwparams;
		snd_pcm_sw_params_t* swparams;
		snd_pcm_status_t* status;
		int pchannels = 2;
		snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE;

		handle = nullptr;
		pcm_callback = nullptr;
		pspeed = SAMPLE_RATE;

		// buffer time and period time are in microseconds...
		// (don't simplify the equation below -- it'll just cause integer rounding errors.
		period_time = (SndOutPacketSize * 1000) / (SampleRate / 1000);
		buffer_time = period_time * NumBuffers;

		int err;

		err = snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, SND_PCM_ASYNC /*| SND_PCM_NONBLOCK*/);
		if (err < 0)
		{
			fprintf(stderr, "Audio open error: %s\n", snd_strerror(err));
			return -1;
		}

		err = snd_pcm_nonblock(handle, 0);
		if (err < 0)
		{
			fprintf(stderr, "Can't set blocking mode: %s\n", snd_strerror(err));
			return -1;
		}

		snd_pcm_hw_params_alloca(&hwparams);
		snd_pcm_sw_params_alloca(&swparams);

		err = snd_pcm_hw_params_any(handle, hwparams);
		if (err < 0)
		{
			fprintf(stderr, "Broken configuration for this PCM: %s\n", snd_strerror(err));
			return -1;
		}

		err = snd_pcm_hw_params_set_access(handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
		if (err < 0)
		{
			fprintf(stderr, "Access type not available: %s\n", snd_strerror(err));
			return -1;
		}

		err = snd_pcm_hw_params_set_format(handle, hwparams, format);
		if (err < 0)
		{
			fprintf(stderr, "Sample format not available: %s\n", snd_strerror(err));
			return -1;
		}

		err = snd_pcm_hw_params_set_channels(handle, hwparams, pchannels);
		if (err < 0)
		{
			fprintf(stderr, "Channels count not available: %s\n", snd_strerror(err));
			return -1;
		}
		err = snd_pcm_hw_params_set_rate_near(handle, hwparams, &pspeed, 0);
		if (err < 0)
		{
			fprintf(stderr, "Rate not available: %s\n", snd_strerror(err));
			return -1;
		}

		err = snd_pcm_hw_params_set_buffer_time_near(handle, hwparams, &buffer_time, 0);
		if (err < 0)
		{
			fprintf(stderr, "Buffer time error: %s\n", snd_strerror(err));
			return -1;
		}

		err = snd_pcm_hw_params_set_period_time_near(handle, hwparams, &period_time, 0);
		if (err < 0)
		{
			fprintf(stderr, "Period time error: %s\n", snd_strerror(err));
			return -1;
		}

		err = snd_pcm_hw_params(handle, hwparams);
		if (err < 0)
		{
			fprintf(stderr, "Unable to install hw params: %s\n", snd_strerror(err));
			return -1;
		}

		snd_pcm_status_alloca(&status);
		err = snd_pcm_status(handle, status);
		if (err < 0)
		{
			fprintf(stderr, "Unable to get status: %s\n", snd_strerror(err));
			return -1;
		}

		// Bind our asynchronous callback magic:

		if (handle == nullptr)
			fprintf(stderr, "No handle.");

		//fprintf(stderr,"* SPU2:Iz setting your internal callback.\n");
		// The external handler never seems to get called after this.
		snd_async_add_pcm_handler(&pcm_callback, handle, ExternalCallback, this);
		err = snd_pcm_start(handle);
		if (err < 0)
		{
			fprintf(stderr, "Pcm start failed: %s\n", snd_strerror(err));
			return -1;
		}
		// Diagnostic code:
		//buffer_size = snd_pcm_status_get_avail(status);

		//fprintf(stderr,"All set up.\n");
		return 0;
	}

	void Close()
	{
		//fprintf(stderr,"* SPU2: Closing Alsa\n");
		if (handle == nullptr)
			return;

		snd_pcm_drop(handle);
		snd_pcm_close(handle);
		handle = nullptr;
	}

	virtual void Configure(uptr parent)
	{
	}

	virtual bool Is51Out() const { return false; }

	s32 Test() const
	{
		return 0;
	}

	int GetEmptySampleCount()
	{
		if (handle == nullptr)
		{
			fprintf(stderr, "Handle is nullptr!\n");
			return 0;
		}

		// Returns the amount of free buffer space, in samples.
		int l = snd_pcm_avail_update(handle);
		if (l < 0)
			return 0;
		return (l / 1000) * (SampleRate / 1000);
	}

	const wchar_t* GetIdent() const
	{
		return L"Alsa";
	}

	const wchar_t* GetLongName() const
	{
		return L"Alsa";
	}

	void ReadSettings()
	{
	}

	void SetApiSettings(wxString api)
	{
	}

	void WriteSettings() const
	{
	}
} static Alsa;

SndOutModule* AlsaOut = &Alsa;

#endif
